from dataclasses import dataclass
from datetime import datetime
from typing import Callable, Union, Type, List
from emi.core.MainEventEngine import MainEventEngine
from emi.core.Context import Context, ContextWrapper
from emi.core.Runner import Runner, RunnerContext, RunnerScheduler, Runnable

__all__ = [
    # Super-special typing primitives.
    'RunnerManager',
]


TAG = "Runner_manager"

def parse_hour_minute_second(text:str):
    digit_list = text.split(":")
    assert len(digit_list) == 3
    hour = int(digit_list[0])
    minute = int(digit_list[1])
    sencond = int(digit_list[2])
    assert  0<= hour and hour < 24
    assert  0<= minute and minute < 60
    assert  0<= sencond and sencond < 60
    return hour,minute,sencond


def parse_week_desc(text:str):
    ##格式支持三种： "1", "2-5", "1,3,6"
    digit_list = text.split("-")
    if len(digit_list) == 2:
        #格式: "2-5"
        begin = int(digit_list[0])
        end = int (digit_list[1])
        assert  begin>=1
        assert  end <=7
        return list(range(begin,end + 1))
    digit_list = text.split(",")

    number_list = []
    for digit_text in digit_list:
        try:
            num = int(digit_text)
            assert 1<=num and num <= 7
            number_list.append(num)
        except Exception:
            pass
    if len(number_list) < 1:
        raise RuntimeError(f"parse_week_desc error: {text}")

    return number_list

def parse_day_desc(text:str):
    ##格式支持三种： "1", "2-5", "1,3,6"
    digit_list = text.split("-")
    if len(digit_list) == 2:
        #格式: "2-5"
        begin = int(digit_list[0])
        end = int (digit_list[1])
        assert  begin>=1
        assert  end <=31
        return list(range(begin,end + 1))
    digit_list = text.split(",")

    number_list = []
    for digit_text in digit_list:
        try:
            num = int(digit_text)
            assert 1<=num and num <= 31
            number_list.append(num)
        except Exception:
            pass
    if len(number_list) < 1:
        raise RuntimeError(f"parse_day_desc error: {text}")

    return number_list

def __secheduleToayJob__(context:Context, hour_minute_second:str, now:datetime, function:Callable, args:{}, run_if_miss_time:bool):
    hour, minute, second = parse_hour_minute_second(hour_minute_second)
    job_time = datetime(year=now.year, month=now.month, day=now.day, hour=hour, minute=minute, second=second)
    delay_second = int((job_time.timestamp() - now.timestamp() + 0.45))

    ##错过今天的运行时间
    if delay_second < 0 and not run_if_miss_time :
        return
    context.post_delay(delay_second, function, args)


@dataclass
class Run_Monthly_Job:
    day_desc:str
    hour_minute_second:str
    function:Callable
    args:{}
    run_if_miss_time:bool

    def secheduleToayJob(self, context:Context):
        day_list = parse_day_desc(self.day_desc)
        now = context.now()
        the_day = now.day
        is_match = day_list.__contains__(the_day)
        if is_match:
            __secheduleToayJob__(context, self.hour_minute_second, now, self.function, self.args, self.run_if_miss_time)

@dataclass
class Run_Weekly_Job:
    week_desc:str
    hour_minute_second:str
    function:Callable
    args:{}
    run_if_miss_time:bool

    def secheduleToayJob(self, context:Context):
        week_list = parse_week_desc(self.week_desc)
        now = context.now()
        the_week = now.weekday()  ##  0-6
        the_week+=1
        is_match = week_list.__contains__(the_week)
        if is_match:
            __secheduleToayJob__(context, self.hour_minute_second, now, self.function, self.args, self.run_if_miss_time)


@dataclass
class Run_Daily_Job:
    hour_minute_second:str
    function:Callable
    args:{}
    run_if_miss_time:bool

    def secheduleToayJob(self, context:Context):
        """
        hour_minute_second:
        """
        now = context.now()
        __secheduleToayJob__(context,self.hour_minute_second,now,self.function,self.args,self.run_if_miss_time)


class _RunnerSession(RunnerContext, RunnerScheduler):

    def __init__(self, runner:Runner, ower_context:Context):
        ContextWrapper.__init__(self,ower_context,runner)
        assert runner.context is None
        self.engine = ower_context.engine
        self.runner:Runner = runner
        self.runner.context = self
        self.reset()

    def reset(self):
        self.run_daily_job_list: ['Run_Daily_Job'] = []
        self.run_weekly_job_list: ['Run_Weekly_Job'] = []
        self.run_montly_job_list: ['Run_Monthly_Job'] = []

    def now(self)->datetime:
        return self.engine.now()

    def is_backtest(self)->bool:
        return self.engine.is_backtest

    def run_daily(self,hour_minute_seconde:str,function:Callable, args = {}, run_if_miss_time = False):
        job = Run_Daily_Job(hour_minute_second=hour_minute_seconde,
                            function=function,args=args,run_if_miss_time=run_if_miss_time)
        self.run_daily_job_list.append(job)

    def run_weekly(self,week_desc:str,hour_minute_second:str,function:Callable, args = {}, run_if_miss_time = False):

        job = Run_Weekly_Job(week_desc=week_desc,hour_minute_second=hour_minute_second,
                             function=function,args=args,run_if_miss_time=run_if_miss_time)
        self.run_weekly_job_list.append(job)

    def run_monthly(self,day_desc:str,hour_minute_second:str,function:Callable, args = {}, run_if_miss_time = False):
        job = Run_Monthly_Job(day_desc=day_desc, hour_minute_second=hour_minute_second,
                             function=function, args=args, run_if_miss_time=run_if_miss_time)
        self.run_montly_job_list.append(job)

    def secheduleToayJob(self):
        [ job.secheduleToayJob(self) for job in self.run_daily_job_list]  ##规划daily任务
        [ job.secheduleToayJob(self) for job in self.run_weekly_job_list] ##规划weekly任务
        [ job.secheduleToayJob(self) for job in self.run_montly_job_list] ##规划Monthly任务

class RunnerManager:

    def __init__(self,context:Context):
        self.context = context
        self.engine:MainEventEngine = context.engine
        self.runner_list:['_RunnerSession'] = []
        self.engine.register(MainEventEngine.EVNET_START,self._onStart)
        self.engine.register(MainEventEngine.EVNET_END,self._onStop)
        self.engine.register(MainEventEngine.EVNET_EXCEPTION,self._onException)


    def onInerceptCallable(self,callbale:Callable,args:{}):
        #TODO 分配到线程池里开多线程执行。
        try:
            return callbale(**args)
        except Exception as e:
            session_data = self.engine.getProcessSessionData()
            if not session_data is None and isinstance(session_data,Runner):
                ###拦截Runner异常给具体的Runner的实例方法
                runner = session_data
                runner.onError(e)
                self.engine.cancel_all(session_data)
                self._remove_runner(runner)
                return False
            raise e

    def _onException(self,event:str,exception):
        """
        主线程环境的异常。
        """
        self.context._logger.error("main engine error!!",exc_info = True)
        self.engine.unregister(MainEventEngine.EVNET_DAY_CHANED,self._onDayChanged)
        self.engine.intercept_callbale_handler = None


    def _onStart(self,event:str,engine:MainEventEngine):
        """
        启动,开始安排工作
        """
        self.context.log_d(TAG,"onStart")
        self.engine.register(MainEventEngine.EVNET_DAY_CHANED,self._onDayChanged)
        for runner_session in self.runner_list:
            runner_session.reset()
            runner_session.runner.onStart(runner_session)
        ##拦截引擎的callable方法调用
        if not engine.intercept_callbale_handler is None:
            raise RuntimeError("only one RunnerManager enable,please use App.getRunnerManager()")
        self.engine.intercept_callbale_handler = self.onInerceptCallable
        self._secheduleToayJob()

    def _onStop(self,event:str,engine:MainEventEngine):
        self.context.log_d(TAG, "onStop")
        self.engine.unregister(MainEventEngine.EVNET_DAY_CHANED,self._onDayChanged)
        for runner_session in self.runner_list:
            runner_session.runner.onStop()
            runner_session.reset()
        self.engine.intercept_callbale_handler = None


    def _onDayChanged(self, event: str, theEngine: MainEventEngine):
        """
        天数变化：新的一天开始安排工作。
        """
        self.context.log_d(TAG, "_onDayChanged")
        self._secheduleToayJob()

    def _secheduleToayJob(self):
        """
         分配今天的任务
        """
        self.context.log_d(TAG, "_secheduleToayJob")
        [ runner_wrapper.secheduleToayJob() for runner_wrapper in self.runner_list ]


    def submit(self, runner: Union[Runner,Type[Runner], Runnable, Type[Runnable]]):
        """
        提交Runner执行。
        """
        # if self.engine.is_running():
        #     raise RuntimeError("can't add while is main engine runing!")
        runner = self.__get_runner_instance(runner)
        for runner_wrapper in self.runner_list:
            if runner_wrapper.runner.getName() == runner.getName():
                raise RuntimeError(f"runner.name [{runner.getName()}] 已经存在！")
        self.runner_list.append(_RunnerSession(runner, self.context))

    def get_runners(self)->List[Runner]:
        return [ r_session.runner for r_session in self.runner_list]

    def cancel(self, runner: Union[Runner,Type[Runner], Runnable, Type[Runnable]]):
        this = self
        runner = self.__get_runner_instance(runner)
        def __inter_cancel():
            for runner_wrapper in this.runner_list:
                if runner_wrapper.runner.getName() == runner.getName():
                    this.engine.cancel_all(runner_wrapper.runner)
                    this._remove_runner(runner)
                    runner_wrapper.runner.onStop()

        self.engine.postDelay(0, __inter_cancel)




    def __get_runner_instance(self,runner: Union[Runner,Type[Runner], Runnable, Type[Runnable]])->Runner:
        try:
            if issubclass(runner, Runnable):
                runner = runner()._create_runner_()
                return runner
        except TypeError:
            pass
        try:
            if issubclass(runner, Runner):
                runner = runner()
                return runner
        except TypeError:
            pass
        if isinstance(runner, Runnable):
            runner = runner._create_runner_()
        assert isinstance(runner, Runner)
        return runner

    def _remove_runner(self,runner:Runner):
        target_object = None
        for runner_wrapper in self.runner_list:
            if runner_wrapper.runner.getName() == runner.getName():
                target_object = runner_wrapper
        if not target_object is None:
            self.runner_list.remove(target_object)

    def clear(self):
        self.runner_list.clear()

if __name__ == "__main__":
    from emi.core.App import App
    app = App()

    hour,minute,second = parse_hour_minute_second("15:23:34")
    print(f" parse_week_desc(2-2）：  {parse_week_desc('2-2')}")
    print(f" parse_week_desc(3-6）：  {parse_week_desc('3-6')}")
    print(f" parse_week_desc(2）：  {parse_week_desc('2')}")
    print(f" parse_week_desc(4,3,5,2）：  {parse_week_desc('4,3,5,2')}")
    print(f" parse_week_desc(4,）：  {parse_week_desc('4,')}")

    ##格式支持三种： "1", "2-5", "1,3,6"

    class MyRunner(Runner):

        def getName(self):
            return "MyRunner"

        def onStart(self, scheduler: RunnerScheduler):
            self.log("onStartUp")

            scheduler.run_weekly("3,4,6","5:4:6",self.on_RunAt_Weekly_5_4_6,args={},run_if_miss_time=True)
            scheduler.run_daily("15:23:34",self.onRunAt_15_23_34,args={},run_if_miss_time=False)
            scheduler.run_daily("15:23:35",self.onRunAt_15_23_35,args={},run_if_miss_time=False)

            scheduler.run_monthly("7,4,10,9,14","1:11:11",self.on_RunAt_Monthly_1_11_11,args={},run_if_miss_time=False)

        def onRunAt_15_23_34(self):
            assert self.context.getStorage().daily_data().get("xxxx",None) is None
            self.context.getStorage().daily_data().put("xxxx","345")
            ##raise RuntimeError("sfsfsf")
            self.log("onRunAt_15_23_34")

        def onRunAt_15_23_35(self):
            assert self.context.getStorage().daily_data().get("xxxx",None)  == "345"

        def on_RunAt_Weekly_5_4_6(self):
            now = self.context.now();
            self.log(f"on_RunAt_Weekly_5_4_6, 星期{now.weekday()+1}")
            self.context.post_delay(40,self.on_RunAt_Weekly_5_4_6_delay_at_40s)

        def on_RunAt_Weekly_5_4_6_delay_at_40s(self):
            self.log("on_RunAt_Weekly_5_4_6_delay_at_40s")

        def on_RunAt_Monthly_1_11_11(self):
            self.log("on_RunAt_Monthly_1_11_11")

        def onStop(self):
            self.log("onStop==>")
            pass


    class MyRunner2(Runner):

        def getName(self):
            return "MyRunner2"

        def onStart(self, scheduler: RunnerScheduler):
            self.log("onStartUp")
            self.count  =0
            scheduler.run_daily("15:23:34",self.onRunAt_15_23_34,args={},run_if_miss_time=False)

        def onError(self, exception):
            self.log("onError=============")
            self.log("onError=============")
            self.log("onError=============")
            self.log("onError=============")
            app.getRunnerManager().cancel(MyRunner)

        def onRunAt_15_23_34(self):
            ##raise RuntimeError("sfsfsf")
            self.log("onXXXXXX=============")
            self.count +=1
            if self.count > 10:
                raise RuntimeError("hahahahah1")

    start = datetime(year=2021, month=1, day=12, hour=14)

    runnerManager = app.getRunnerManager()
    runnerManager.submit(MyRunner())
    runnerManager.submit(MyRunner2())
    app.run_backtest(start)

    app.engine.go(3600*24*10)
    app.wait_backtest_finished(True)
